@objectstack/runtime 6.9.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1294,7 +1294,15 @@ var init_app_plugin = __esm({
1294
1294
  AppPlugin = class {
1295
1295
  constructor(bundle, projectContext) {
1296
1296
  this.type = "app";
1297
+ /** When true, init/start become no-ops — env has no app payload. */
1298
+ this.empty = false;
1297
1299
  this.init = async (ctx) => {
1300
+ if (this.empty) {
1301
+ ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping init", {
1302
+ pluginName: this.name
1303
+ });
1304
+ return;
1305
+ }
1298
1306
  const sys = this.bundle.manifest || this.bundle;
1299
1307
  const appId = sys.id || sys.name;
1300
1308
  ctx.logger.info("Registering App Service", {
@@ -1309,6 +1317,12 @@ var init_app_plugin = __esm({
1309
1317
  ctx.getService("manifest").register(servicePayload);
1310
1318
  };
1311
1319
  this.start = async (ctx) => {
1320
+ if (this.empty) {
1321
+ ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping start", {
1322
+ pluginName: this.name
1323
+ });
1324
+ return;
1325
+ }
1312
1326
  const sys = this.bundle.manifest || this.bundle;
1313
1327
  const appId = sys.id || sys.name;
1314
1328
  let ql;
@@ -1604,47 +1618,64 @@ var init_app_plugin = __esm({
1604
1618
  if (multiTenant) {
1605
1619
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1606
1620
  } else {
1607
- try {
1608
- const metadata = ctx.getService("metadata");
1609
- if (metadata) {
1610
- const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1611
- const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1612
- const request = SeedLoaderRequestSchema.parse({
1613
- datasets: normalizedDatasets,
1614
- config: { defaultMode: "upsert", multiPass: true }
1615
- });
1616
- const result = await seedLoader.load(request);
1617
- ctx.logger.info("[Seeder] Seed loading complete", {
1618
- inserted: result.summary.totalInserted,
1619
- updated: result.summary.totalUpdated,
1620
- errors: result.errors.length
1621
- });
1622
- } else {
1623
- ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1621
+ const seedBudgetMs = Number(process.env.OS_INLINE_SEED_BUDGET_MS ?? 8e3);
1622
+ const seedPromise = (async () => {
1623
+ try {
1624
+ const metadata = ctx.getService("metadata");
1625
+ if (metadata) {
1626
+ const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1627
+ const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1628
+ const request = SeedLoaderRequestSchema.parse({
1629
+ datasets: normalizedDatasets,
1630
+ config: { defaultMode: "upsert", multiPass: true }
1631
+ });
1632
+ const result = await seedLoader.load(request);
1633
+ ctx.logger.info("[Seeder] Seed loading complete", {
1634
+ inserted: result.summary.totalInserted,
1635
+ updated: result.summary.totalUpdated,
1636
+ errors: result.errors.length
1637
+ });
1638
+ } else {
1639
+ ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1640
+ for (const dataset of normalizedDatasets) {
1641
+ ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1642
+ for (const record of dataset.records) {
1643
+ try {
1644
+ await ql.insert(dataset.object, record, { context: { isSystem: true } });
1645
+ } catch (err) {
1646
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
1647
+ }
1648
+ }
1649
+ }
1650
+ ctx.logger.info("[Seeder] Data seeding complete.");
1651
+ }
1652
+ } catch (err) {
1653
+ ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1624
1654
  for (const dataset of normalizedDatasets) {
1625
- ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1626
1655
  for (const record of dataset.records) {
1627
1656
  try {
1628
1657
  await ql.insert(dataset.object, record, { context: { isSystem: true } });
1629
- } catch (err) {
1630
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
1658
+ } catch (insertErr) {
1659
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1631
1660
  }
1632
1661
  }
1633
1662
  }
1634
- ctx.logger.info("[Seeder] Data seeding complete.");
1663
+ ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1635
1664
  }
1636
- } catch (err) {
1637
- ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1638
- for (const dataset of normalizedDatasets) {
1639
- for (const record of dataset.records) {
1640
- try {
1641
- await ql.insert(dataset.object, record, { context: { isSystem: true } });
1642
- } catch (insertErr) {
1643
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1644
- }
1645
- }
1646
- }
1647
- ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1665
+ })();
1666
+ let timer;
1667
+ const budget = new Promise((resolve2) => {
1668
+ timer = setTimeout(() => resolve2("budget"), seedBudgetMs);
1669
+ });
1670
+ const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
1671
+ if (timer) clearTimeout(timer);
1672
+ if (winner === "budget") {
1673
+ ctx.logger.warn(
1674
+ `[Seeder] Inline seed exceeded ${seedBudgetMs}ms budget for ${appId}; continuing in background to avoid blocking kernel start.`
1675
+ );
1676
+ seedPromise.catch((err) => {
1677
+ ctx.logger.warn("[Seeder] Background seed failed after budget", { appId, error: err?.message ?? String(err) });
1678
+ });
1648
1679
  }
1649
1680
  }
1650
1681
  }
@@ -1658,6 +1689,38 @@ var init_app_plugin = __esm({
1658
1689
  const sys = bundle?.manifest || bundle;
1659
1690
  const appId = sys?.id || sys?.name;
1660
1691
  if (!appId) {
1692
+ const APP_CATEGORY_KEYS = [
1693
+ "objects",
1694
+ "views",
1695
+ "apps",
1696
+ "pages",
1697
+ "dashboards",
1698
+ "reports",
1699
+ "flows",
1700
+ "workflows",
1701
+ "triggers",
1702
+ "agents",
1703
+ "tools",
1704
+ "skills",
1705
+ "actions",
1706
+ "permissions",
1707
+ "roles",
1708
+ "profiles",
1709
+ "translations",
1710
+ "sharingRules",
1711
+ "ragPipelines",
1712
+ "data"
1713
+ ];
1714
+ const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
1715
+ const v = (bundle && bundle[k]) ?? (sys && sys[k]);
1716
+ return Array.isArray(v) && v.length > 0;
1717
+ });
1718
+ if (!hasAppPayload) {
1719
+ this.empty = true;
1720
+ const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
1721
+ this.name = `plugin.app.empty-${envSlug}`;
1722
+ return;
1723
+ }
1661
1724
  const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1662
1725
  const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1663
1726
  const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
@@ -1666,7 +1729,7 @@ var init_app_plugin = __esm({
1666
1729
  source: projectContext.source
1667
1730
  })}` : "";
1668
1731
  throw new Error(
1669
- `[AppPlugin] bundle is missing manifest.id and manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1732
+ `[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1670
1733
  );
1671
1734
  }
1672
1735
  this.name = `plugin.app.${appId}`;
@@ -2283,7 +2346,7 @@ function resolveObjectStackHome() {
2283
2346
  var StandaloneStackConfigSchema = import_zod.z.object({
2284
2347
  databaseUrl: import_zod.z.string().optional(),
2285
2348
  databaseAuthToken: import_zod.z.string().optional(),
2286
- databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "turso", "memory", "postgres", "mongodb"]).optional(),
2349
+ databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2287
2350
  environmentId: import_zod.z.string().optional(),
2288
2351
  artifactPath: import_zod.z.string().optional(),
2289
2352
  /**
@@ -2301,7 +2364,6 @@ var StandaloneStackConfigSchema = import_zod.z.object({
2301
2364
  });
2302
2365
  function detectDriverFromUrl(dbUrl) {
2303
2366
  if (/^memory:\/\//i.test(dbUrl)) return "memory";
2304
- if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
2305
2367
  if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2306
2368
  if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2307
2369
  if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
@@ -2309,7 +2371,7 @@ function detectDriverFromUrl(dbUrl) {
2309
2371
  if (/^file:/i.test(dbUrl)) return "sqlite";
2310
2372
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2311
2373
  throw new Error(
2312
- `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, libsql://, https://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2374
+ `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2313
2375
  );
2314
2376
  }
2315
2377
  async function createStandaloneStack(config) {
@@ -2323,25 +2385,12 @@ async function createStandaloneStack(config) {
2323
2385
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
2324
2386
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
2325
2387
  const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path2.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
2326
- const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2327
2388
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2328
2389
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2329
2390
  let driverPlugin;
2330
2391
  if (dbDriver === "memory") {
2331
2392
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
2332
2393
  driverPlugin = new DriverPlugin2(new InMemoryDriver());
2333
- } else if (dbDriver === "turso") {
2334
- let TursoDriver;
2335
- try {
2336
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
2337
- } catch (err) {
2338
- throw new Error(
2339
- `[StandaloneStack] libsql/turso URL detected ("${dbUrl}") but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso (or use a file: URL to default to better-sqlite3). (${err?.message ?? err})`
2340
- );
2341
- }
2342
- driverPlugin = new DriverPlugin2(
2343
- new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
2344
- );
2345
2394
  } else if (dbDriver === "postgres") {
2346
2395
  const { SqlDriver } = await import("@objectstack/driver-sql");
2347
2396
  driverPlugin = new DriverPlugin2(
@@ -4082,7 +4131,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4082
4131
  return {
4083
4132
  handled: true,
4084
4133
  response: this.error(
4085
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or TursoDriver).",
4134
+ "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
4086
4135
  503
4087
4136
  )
4088
4137
  };
@@ -7574,6 +7623,27 @@ function extractRuntimeFromMetadata(metadata) {
7574
7623
  }
7575
7624
  async function createDriver(driverType, databaseUrl, authToken) {
7576
7625
  switch (driverType) {
7626
+ case "libsql":
7627
+ case "turso": {
7628
+ let TursoDriver;
7629
+ try {
7630
+ ({ TursoDriver } = await import("@objectstack/driver-turso"));
7631
+ } catch (primaryErr) {
7632
+ try {
7633
+ const { createRequire } = await import("module");
7634
+ const path = await import("path");
7635
+ const url = await import("url");
7636
+ const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
7637
+ const resolved = hostRequire.resolve("@objectstack/driver-turso");
7638
+ ({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
7639
+ } catch (fallbackErr) {
7640
+ throw new Error(
7641
+ `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not resolvable. Install it from the cloud monorepo (cloud/packages/driver-turso) or via npm. (primary: ${primaryErr?.message ?? primaryErr}; fallback: ${fallbackErr?.message ?? fallbackErr})`
7642
+ );
7643
+ }
7644
+ }
7645
+ return new TursoDriver({ url: databaseUrl, authToken });
7646
+ }
7577
7647
  case "memory": {
7578
7648
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
7579
7649
  const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
@@ -7592,18 +7662,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
7592
7662
  useNullAsDefault: true
7593
7663
  });
7594
7664
  }
7595
- case "libsql":
7596
- case "turso": {
7597
- let TursoDriver;
7598
- try {
7599
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
7600
- } catch (err) {
7601
- throw new Error(
7602
- `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
7603
- );
7604
- }
7605
- return new TursoDriver({ url: databaseUrl, authToken });
7606
- }
7607
7665
  case "postgres":
7608
7666
  case "postgresql":
7609
7667
  case "pg": {
@@ -7879,9 +7937,20 @@ var ArtifactKernelFactory = class {
7879
7937
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7880
7938
  }
7881
7939
  try {
7882
- const { SecurityPlugin } = await import("@objectstack/plugin-security");
7883
7940
  const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7884
- await kernel.use(new SecurityPlugin({ multiTenant }));
7941
+ if (multiTenant) {
7942
+ try {
7943
+ const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
7944
+ await kernel.use(new OrgScopingPlugin());
7945
+ } catch (err) {
7946
+ this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
7947
+ environmentId,
7948
+ error: err?.message
7949
+ });
7950
+ }
7951
+ }
7952
+ const { SecurityPlugin } = await import("@objectstack/plugin-security");
7953
+ await kernel.use(new SecurityPlugin());
7885
7954
  } catch (err) {
7886
7955
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
7887
7956
  environmentId,
@@ -8407,6 +8476,40 @@ function resolveCloudUrl(explicit) {
8407
8476
 
8408
8477
  // src/cloud/marketplace-proxy-plugin.ts
8409
8478
  var MARKETPLACE_PREFIX = "/api/v1/marketplace";
8479
+ var DEFAULT_LRU_MAX = 200;
8480
+ var LIST_TTL_MS = 30 * 60 * 1e3;
8481
+ var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
8482
+ var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
8483
+ function ttlForPath(pathname) {
8484
+ if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
8485
+ if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
8486
+ return LIST_TTL_MS;
8487
+ }
8488
+ var LruTtlCache = class {
8489
+ constructor(max) {
8490
+ this.max = max;
8491
+ this.map = /* @__PURE__ */ new Map();
8492
+ }
8493
+ get(key) {
8494
+ const entry = this.map.get(key);
8495
+ if (!entry) return void 0;
8496
+ this.map.delete(key);
8497
+ this.map.set(key, entry);
8498
+ return entry;
8499
+ }
8500
+ set(key, entry) {
8501
+ if (this.map.has(key)) this.map.delete(key);
8502
+ this.map.set(key, entry);
8503
+ while (this.map.size > this.max) {
8504
+ const oldest = this.map.keys().next().value;
8505
+ if (oldest === void 0) break;
8506
+ this.map.delete(oldest);
8507
+ }
8508
+ }
8509
+ clear() {
8510
+ this.map.clear();
8511
+ }
8512
+ };
8410
8513
  var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8411
8514
  constructor(config = {}) {
8412
8515
  this.name = "com.objectstack.runtime.marketplace-proxy";
@@ -8428,6 +8531,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8428
8531
  }
8429
8532
  const rawApp = httpServer.getRawApp();
8430
8533
  const cloudUrl = this.cloudUrl;
8534
+ const cache = this.cache;
8431
8535
  const handler = async (c, next) => {
8432
8536
  if (!cloudUrl) {
8433
8537
  return c.json({
@@ -8454,24 +8558,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8454
8558
  }
8455
8559
  }, 405);
8456
8560
  }
8457
- const resp = await fetch(target, {
8458
- method,
8459
- headers: {
8460
- // Strip the inbound Host header fetch will set
8461
- // it to the cloud host. Forward only the
8462
- // identifying headers cloud might log.
8463
- "Accept": c.req.header("accept") ?? "application/json",
8464
- "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8561
+ const accept = c.req.header("accept") ?? "application/json";
8562
+ const acceptLang = c.req.header("accept-language") ?? "";
8563
+ const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
8564
+ const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
8565
+ const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
8566
+ const now = Date.now();
8567
+ if (cache && !bypass) {
8568
+ const hit = cache.get(cacheKey);
8569
+ if (hit && hit.expiresAt > now) {
8570
+ return buildCachedResponse(hit, method, "HIT");
8465
8571
  }
8466
- });
8467
- const headers = new Headers();
8468
- const passthroughHeaders = ["content-type", "cache-control", "etag", "last-modified"];
8469
- for (const h of passthroughHeaders) {
8470
- const v = resp.headers.get(h);
8471
- if (v) headers.set(h, v);
8572
+ if (hit) {
8573
+ const revalHeaders = {
8574
+ "Accept": accept,
8575
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8576
+ };
8577
+ if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
8578
+ if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
8579
+ if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
8580
+ const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
8581
+ if (revalResp.status === 304) {
8582
+ hit.expiresAt = now + hit.ttlMs;
8583
+ const newEtag = revalResp.headers.get("etag");
8584
+ const newLm = revalResp.headers.get("last-modified");
8585
+ if (newEtag) hit.etag = newEtag;
8586
+ if (newLm) hit.lastModified = newLm;
8587
+ cache.set(cacheKey, hit);
8588
+ return buildCachedResponse(hit, method, "REVALIDATED");
8589
+ }
8590
+ return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
8591
+ }
8592
+ }
8593
+ const reqHeaders = {
8594
+ // Strip the inbound Host header — fetch will set
8595
+ // it to the cloud host. Forward only the
8596
+ // identifying headers cloud might log.
8597
+ "Accept": accept,
8598
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8599
+ };
8600
+ if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
8601
+ const resp = await fetch(target, { method: "GET", headers: reqHeaders });
8602
+ if (bypass || !cache) {
8603
+ return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
8472
8604
  }
8473
- const body = await resp.arrayBuffer();
8474
- return new Response(body, { status: resp.status, headers });
8605
+ return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
8475
8606
  } catch (err) {
8476
8607
  const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
8477
8608
  ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
@@ -8494,12 +8625,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8494
8625
  }
8495
8626
  }
8496
8627
  }
8497
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
8628
+ ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
8498
8629
  });
8499
8630
  };
8500
8631
  this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
8632
+ const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
8633
+ const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
8634
+ const disabled = config.cacheDisabled ?? envDisabled;
8635
+ this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
8501
8636
  }
8502
8637
  };
8638
+ var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
8639
+ function collectHeaders(src) {
8640
+ const out = {};
8641
+ for (const h of PASSTHROUGH_HEADERS) {
8642
+ const v = src.headers.get(h);
8643
+ if (v) out[h] = v;
8644
+ }
8645
+ return out;
8646
+ }
8647
+ function buildCachedResponse(entry, method, xCache) {
8648
+ const headers = new Headers(entry.headers);
8649
+ headers.set("X-Cache", xCache);
8650
+ const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
8651
+ headers.set("Age", String(Math.max(0, ageSec)));
8652
+ const body = method === "HEAD" ? null : entry.body;
8653
+ return new Response(body, { status: entry.status, headers });
8654
+ }
8655
+ async function passthroughResponse(resp, method, xCache) {
8656
+ const headers = new Headers(collectHeaders(resp));
8657
+ headers.set("X-Cache", xCache);
8658
+ if (method === "HEAD") {
8659
+ try {
8660
+ await resp.arrayBuffer();
8661
+ } catch {
8662
+ }
8663
+ return new Response(null, { status: resp.status, headers });
8664
+ }
8665
+ const body = await resp.arrayBuffer();
8666
+ return new Response(body, { status: resp.status, headers });
8667
+ }
8668
+ async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
8669
+ const body = await resp.arrayBuffer();
8670
+ const headers = collectHeaders(resp);
8671
+ if (resp.status >= 200 && resp.status < 300) {
8672
+ const ttlMs = ttlForPath(pathname);
8673
+ const entry = {
8674
+ status: resp.status,
8675
+ body,
8676
+ headers,
8677
+ etag: resp.headers.get("etag") ?? void 0,
8678
+ lastModified: resp.headers.get("last-modified") ?? void 0,
8679
+ expiresAt: Date.now() + ttlMs,
8680
+ ttlMs
8681
+ };
8682
+ cache.set(key, entry);
8683
+ }
8684
+ const respHeaders = new Headers(headers);
8685
+ respHeaders.set("X-Cache", "MISS");
8686
+ const outBody = method === "HEAD" ? null : body;
8687
+ return new Response(outBody, { status: resp.status, headers: respHeaders });
8688
+ }
8503
8689
 
8504
8690
  // src/cloud/runtime-config-plugin.ts
8505
8691
  var RuntimeConfigPlugin = class {
@@ -8537,12 +8723,14 @@ var RuntimeConfigPlugin = class {
8537
8723
  let defaultEnvironmentId;
8538
8724
  let defaultOrgId;
8539
8725
  let resolvedSingleEnv = this.singleEnvironment;
8540
- if (envRegistry && host && typeof envRegistry.resolveHostname === "function") {
8726
+ const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
8727
+ if (resolveFn && host) {
8541
8728
  try {
8542
- const resolved = await envRegistry.resolveHostname(host);
8729
+ const resolved = await resolveFn(host);
8543
8730
  if (resolved?.environmentId) {
8544
- defaultEnvironmentId = resolved.environmentId;
8545
- if (resolved.organizationId) defaultOrgId = String(resolved.organizationId);
8731
+ defaultEnvironmentId = String(resolved.environmentId);
8732
+ const orgId = resolved.organizationId ?? resolved.organization_id;
8733
+ if (orgId) defaultOrgId = String(orgId);
8546
8734
  resolvedSingleEnv = true;
8547
8735
  }
8548
8736
  } catch {
@@ -8553,7 +8741,11 @@ var RuntimeConfigPlugin = class {
8553
8741
  singleEnvironment: resolvedSingleEnv,
8554
8742
  defaultOrgId,
8555
8743
  defaultEnvironmentId,
8556
- features
8744
+ features,
8745
+ branding: {
8746
+ productName: this.productName,
8747
+ productShortName: this.productShortName
8748
+ }
8557
8749
  });
8558
8750
  };
8559
8751
  rawApp.get("/api/v1/runtime/config", handler);
@@ -8570,6 +8762,10 @@ var RuntimeConfigPlugin = class {
8570
8762
  this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
8571
8763
  this.installLocal = !!config.installLocal;
8572
8764
  this.singleEnvironment = !!config.singleEnvironment;
8765
+ const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
8766
+ const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
8767
+ this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
8768
+ this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
8573
8769
  }
8574
8770
  };
8575
8771
 
@@ -8859,9 +9055,15 @@ var MarketplaceInstallLocalPlugin = class {
8859
9055
  const postHandler = async (c) => this.handleInstall(c, ctx);
8860
9056
  const getHandler = async (c) => this.handleList(c);
8861
9057
  const deleteHandler = async (c) => this.handleUninstall(c, ctx);
9058
+ const reseedHandler = async (c) => this.handleReseed(c, ctx);
9059
+ const purgeHandler = async (c) => this.handlePurge(c, ctx);
8862
9060
  if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
8863
9061
  if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
8864
9062
  if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
9063
+ if (typeof rawApp.post === "function") {
9064
+ rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
9065
+ rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
9066
+ }
8865
9067
  ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
8866
9068
  });
8867
9069
  };
@@ -8958,7 +9160,8 @@ var MarketplaceInstallLocalPlugin = class {
8958
9160
  version,
8959
9161
  manifest,
8960
9162
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
8961
- installedBy: userId
9163
+ installedBy: userId,
9164
+ withSampleData: false
8962
9165
  };
8963
9166
  try {
8964
9167
  (0, import_node_fs3.mkdirSync)(this.storageDir, { recursive: true });
@@ -8985,6 +9188,13 @@ var MarketplaceInstallLocalPlugin = class {
8985
9188
  ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
8986
9189
  }
8987
9190
  const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
9191
+ if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
9192
+ entry.withSampleData = true;
9193
+ try {
9194
+ (0, import_node_fs3.writeFileSync)((0, import_node_path6.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9195
+ } catch {
9196
+ }
9197
+ }
8988
9198
  return c.json({
8989
9199
  success: true,
8990
9200
  data: {
@@ -9011,7 +9221,8 @@ var MarketplaceInstallLocalPlugin = class {
9011
9221
  manifestId: e.manifestId,
9012
9222
  version: e.version,
9013
9223
  installedAt: e.installedAt,
9014
- installedBy: e.installedBy
9224
+ installedBy: e.installedBy,
9225
+ withSampleData: e.withSampleData ?? false
9015
9226
  })),
9016
9227
  total: entries.length,
9017
9228
  storageDir: this.storageDir
@@ -9076,6 +9287,145 @@ var MarketplaceInstallLocalPlugin = class {
9076
9287
  * dev / single-tenant runtimes. Stricter checks can be layered on
9077
9288
  * via a middleware in cloud-hosted multi-tenant deployments.
9078
9289
  */
9290
+ /**
9291
+ * POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
9292
+ *
9293
+ * Re-runs SeedLoaderService against the cached manifest's `data` arrays.
9294
+ * Idempotent (upsert by id). Useful when:
9295
+ * • The user installed an app and skipped sample data
9296
+ * • A purge was undone
9297
+ * • The user wants a clean baseline back after editing demo rows
9298
+ *
9299
+ * Multi-tenant: requires an active organization on the session (same
9300
+ * rule as install seed path).
9301
+ */
9302
+ this.handleReseed = async (c, ctx) => {
9303
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9304
+ if (!userId) {
9305
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9306
+ }
9307
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9308
+ if (!manifestId) {
9309
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9310
+ }
9311
+ const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9312
+ if (!(0, import_node_fs3.existsSync)(file)) {
9313
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9314
+ }
9315
+ let entry;
9316
+ try {
9317
+ entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
9318
+ } catch (err) {
9319
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9320
+ }
9321
+ const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
9322
+ if (summary.seeded.mode === "skipped") {
9323
+ return c.json({
9324
+ success: false,
9325
+ error: {
9326
+ code: "reseed_skipped",
9327
+ message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
9328
+ }
9329
+ }, 400);
9330
+ }
9331
+ try {
9332
+ entry.withSampleData = true;
9333
+ (0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
9334
+ } catch {
9335
+ }
9336
+ return c.json({
9337
+ success: true,
9338
+ data: {
9339
+ manifestId,
9340
+ inserted: summary.seeded.inserted ?? 0,
9341
+ updated: summary.seeded.updated ?? 0,
9342
+ errors: summary.seeded.errors ?? 0,
9343
+ withSampleData: true
9344
+ }
9345
+ }, 200);
9346
+ };
9347
+ /**
9348
+ * POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
9349
+ *
9350
+ * Deletes every record whose id is declared in the cached manifest's
9351
+ * seed datasets. Uses the `driver` service directly to bypass ACL /
9352
+ * lifecycle hooks (same pattern as cloud purge). User-created records
9353
+ * are never touched — only ids declared in the package's bundled
9354
+ * datasets are removed. Already-deleted rows count as `skipped`.
9355
+ */
9356
+ this.handlePurge = async (c, ctx) => {
9357
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9358
+ if (!userId) {
9359
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9360
+ }
9361
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9362
+ if (!manifestId) {
9363
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9364
+ }
9365
+ const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9366
+ if (!(0, import_node_fs3.existsSync)(file)) {
9367
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9368
+ }
9369
+ let entry;
9370
+ try {
9371
+ entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
9372
+ } catch (err) {
9373
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9374
+ }
9375
+ const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
9376
+ if (datasets.length === 0) {
9377
+ return c.json({
9378
+ success: false,
9379
+ error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
9380
+ }, 400);
9381
+ }
9382
+ let driver;
9383
+ try {
9384
+ driver = ctx.getService("driver");
9385
+ } catch {
9386
+ }
9387
+ if (!driver || typeof driver.delete !== "function") {
9388
+ return c.json({
9389
+ success: false,
9390
+ error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
9391
+ }, 500);
9392
+ }
9393
+ let deleted = 0;
9394
+ let skipped = 0;
9395
+ let errors = 0;
9396
+ for (const ds of datasets) {
9397
+ const object = String(ds.object);
9398
+ for (const rec of ds.records) {
9399
+ const id = rec?.id;
9400
+ if (id === void 0 || id === null || id === "") {
9401
+ skipped++;
9402
+ continue;
9403
+ }
9404
+ try {
9405
+ const r = await driver.delete(object, id);
9406
+ if (r === false || r === 0 || r?.deleted === 0) skipped++;
9407
+ else deleted++;
9408
+ } catch (err) {
9409
+ const msg = String(err?.message ?? err);
9410
+ if (/not.?found|no row/i.test(msg)) skipped++;
9411
+ else {
9412
+ errors++;
9413
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
9414
+ }
9415
+ }
9416
+ }
9417
+ }
9418
+ try {
9419
+ entry.withSampleData = false;
9420
+ (0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
9421
+ } catch {
9422
+ }
9423
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
9424
+ return c.json({
9425
+ success: true,
9426
+ data: { manifestId, deleted, skipped, errors, withSampleData: false }
9427
+ }, 200);
9428
+ };
9079
9429
  /**
9080
9430
  * Replicate the start-time side-effects that AppPlugin runs for
9081
9431
  * statically-declared apps but the `manifest` service does NOT: